using System;
using System.Collections.Generic;
using System.Diagnostics;
using TAAlert.CommonUtils;

namespace TAAlert.BackTest
{
    struct PerfStatistics
    {
        private double profitLossM;         // Profit/Loss
        private double maxDrawDownM;        // Max DrawDown
        private DateTime startDateM;        // The first date when the strategy assumed a position
        private DateTime endDateM;          // The last date included in the performance.

        public PerfStatistics(double pl, double maxDD, DateTime startDate, DateTime endDate)
        {
            this.profitLossM = pl;
            this.maxDrawDownM = maxDD;
            this.startDateM = startDate;
            this.endDateM = endDate;
        }

        public double ProfitLoss
        {
            get { return this.profitLossM; }
        }
        public double MaxDrawDown
        {
            get { return this.maxDrawDownM; }
        }
        public double Calmar
        {
            get
            {
                if (this.maxDrawDownM != 0)
                    return -this.profitLossM / this.maxDrawDownM;
                else if (this.profitLossM == 0)
                    return this.profitLossM;
                else
                    return Double.PositiveInfinity;

            }
        }

        public DateTime StartDate
        {
            get { return this.startDateM; }
        }
        public DateTime EndDate
        {
            get { return this.endDateM; }
        }
    }

    class PerformanceCalculator
    {
        private static readonly TraceSwitch traceM = new TraceSwitch("PerformanceCalculator", "Debug information for PerformanceCalculator class");
        private SortedList<DateTime, double> pricesM;
        private SortedList<DateTime, int> positionsM;
        private double transactionCostM;
        public enum Frequency { Monthly, Yearly };

        /// <summary>Constructor for ZERO transaction cost.</summary>
        /// <param name="prices">dated list of prices</param>
        /// <param name="positions">dated list of positions</param>
        private PerformanceCalculator(SortedList<DateTime, double> prices, SortedList<DateTime, int> positions)
        {
            this.pricesM = prices;
            this.positionsM = positions;
        }

        /// <summary>Constructor including transaction cost. Store prices and positions and transaction costs for the performance calculations</summary>
        /// <param name="prices">dated list of prices</param>
        /// <param name="positions">dated list of positions</param>
        /// <param name="transactionCost">decimal value of transaction cost PERCENTAGE. e.g. 0.01 for 1% transaction cost</param>
        public PerformanceCalculator(SortedList<DateTime, double> prices, SortedList<DateTime, int> positions, double transactionCost)
            : this(prices, positions)
        {
            this.transactionCostM = transactionCost;
        }

        /// <summary>Calculate strategy profit/loss, max draw-down and Calmar ratio between startDate and endDate</summary>
        /// <param name="startDate">the first date when the position was established (no performance on this date)</param>
        /// <param name="endDate">the last date when the position was closed</param>
        /// <returns>Performance statistics corresponding to the positions and period.</returns>
        public PerfStatistics calculateStatistics(DateTime startDate, DateTime endDate)
        {
            Debug.WriteLineIf(traceM.TraceInfo, "--> Entering PerformanceCalculator.calculateStatistics(" + startDate.ToShortDateString() + ", " + endDate.ToShortDateString() + ")");
            Debug.Indent();
            // Get indexes for price and position lists
            int startPriceIx = this.getPriceIxOfDate(startDate);
            int endPriceIx = this.getPriceIxOfDate(endDate);
            int startPosIx = this.getPositionIxOfDate(startDate);
            int endPosIx = this.getPositionIxOfDate(endDate);

            List<double> drawDown;
            List<double> cumPL;
            PerfStatistics result = this.calculateStatistics(startPriceIx, endPriceIx, startPosIx, endPosIx, out cumPL, out drawDown);

            Debug.Unindent();
            return result;
        }

        /// <summary>Performance calculations inner loop.</summary>
        /// <param name="startPriceIx">The index corresponding to the first date in the price list</param>
        /// <param name="endPriceIx">The index corrsponding to the last date in the price list</param>
        /// <param name="startPosIx">The index corresponding to the first date in the positions list</param>
        /// <param name="endPosIx">The index corrsponding to the last date in the positions list</param>
        /// <param name="cumPL">List with cumulative P/L</param>
        /// <param name="drawDown">List with draw-downs</param>
        /// <returns>(PL, maxDrawDown, firstDate, lastDate)</returns>
        public PerfStatistics calculateStatistics(int startPriceIx, int endPriceIx, int startPosIx, int endPosIx, out List<double> cumPL, out List<double> drawDown)
        {
            Debug.Assert((endPriceIx - startPriceIx) == (endPosIx - startPosIx), "Size missmatch between position and price indexes in calcualteStatistics");
            // Calculate daily PL, cumulative PL, DrawDown and max DrawDown
            int n = endPriceIx - startPriceIx + 1; // including endDate, that is why +1
           
            cumPL = new List<double>(n);
            drawDown = new List<double>(n);
            double cumulPL = 0;
            double maxPL = 0;
            double maxDrawDown = 0;
            cumPL.Add(0);
            drawDown.Add(0);
            Debug.WriteLineIf(traceM.TraceVerbose, "Date, posYest, dPrice, dailyPL, cumPL, DD, maxDD");
            // today's performance is based on yesterday's position and todays price change + todays close transaction costs
            for (int i = 1; i < n; ++i)
            {
                // Calculate daily Profit /Loss : use position which was establish yesterday on close and todays price change
                double posYesterday = positionsM.Values[startPosIx + i - 1];
                double priceYesterday = pricesM.Values[startPriceIx + i - 1];
                double priceToday = pricesM.Values[startPriceIx + i];
                double dailyPL = posYesterday * (priceToday - priceYesterday);

                // Transaction cost calculation
                if (this.transactionCostM > 0)
                {
                    double posToday = positionsM.Values[startPosIx + i];  // position to be set up on today's close. Used ONLY for transaction costs.
                    if (posToday != posYesterday)
                        dailyPL += -1 * Math.Abs(posYesterday - posToday) * priceToday * this.transactionCostM;
                }

                // Calculate statistics
                cumulPL += dailyPL;
                if (cumulPL >= maxPL)
                    maxPL = cumulPL;
                double dd = cumulPL - maxPL;
                if (dd < maxDrawDown)
                    maxDrawDown = dd;
                cumPL.Add(cumulPL);
                drawDown.Add(dd);
                Debug.WriteLineIf(traceM.TraceVerbose, positionsM.Keys[startPosIx + i].ToShortDateString()
                    + ", " + posYesterday + ", " + (priceToday - priceYesterday).ToString(".##") + ", " + dailyPL.ToString(".##")
                    + ", " + cumulPL.ToString(".##") + ", " + dd.ToString("0.##") + ", " + maxDrawDown.ToString("0.##"));
            }

            DateTime theFirstDate = pricesM.Keys[startPriceIx];
            DateTime theLastDate = pricesM.Keys[endPriceIx];
            Debug.WriteLineIf(traceM.TraceInfo, "(" + theFirstDate.ToShortDateString() + ":" + theLastDate.ToShortDateString() + ") PL=" + cumulPL + ", maxDD=" + maxDrawDown + ", processed " + n + " days");
            Debug.Assert(cumPL.Count == n, "Size of cumPL");
            Debug.Assert(drawDown.Count == n, "Size of drawDown");
            return new PerfStatistics(cumulPL, maxDrawDown, theFirstDate, theLastDate);
        }

        /// <summary>Calculate statistics for all intra-periods between startDate and endDate.</summary>
        /// <param name="startDate">the first date when the position was established (no performance on this date)</param>
        /// <param name="endDate">the last date when the position was closed</param>
        /// <param name="freq">Intra-period frequency</param>
        /// <returns>LIst of all intra-period statistics</returns>
        public List<PerfStatistics> calcIntraPeriodStatistics(DateTime startDate, DateTime endDate, Frequency freq)
        {
            Debug.WriteLineIf(traceM.TraceInfo, "--> Entering calcIntraPeriodStatistics(" + startDate.ToShortDateString() + ", " + endDate.ToShortDateString() + ", " + freq.ToString());
            Debug.Indent();

            List<PerfStatistics> result = new List<PerfStatistics>();

            int startPosIx = this.getPositionIxOfDate(startDate);
            int endPosIx = this.getPositionIxOfDate(endDate);

            DateTime periodStart = positionsM.Keys[startPosIx];
            for (int ix = startPosIx + 1; ix <= endPosIx; ++ix)
            {
                DateTime periodEnd = positionsM.Keys[ix];
                if (ix == endPosIx ||
                    (freq == Frequency.Monthly && periodEnd.Month != positionsM.Keys[ix + 1].Month) ||
                    (freq == Frequency.Yearly && periodEnd.Year != positionsM.Keys[ix + 1].Year))
                {
                    PerfStatistics inPerStat = this.calculateStatistics(periodStart, periodEnd);
                    result.Add(inPerStat);
                    Debug.WriteLineIf(traceM.TraceInfo, "period (" + inPerStat.StartDate.ToShortDateString() + ", " + inPerStat.EndDate.ToShortDateString() + ") PL=" + inPerStat.ProfitLoss + ", maxDD=" + inPerStat.MaxDrawDown + ", Calmar=" + inPerStat.Calmar + ")");
                    periodStart = periodEnd;
                }
            }
            Debug.Unindent();
            return result;
        }

        /// <summary>Find date's index in position SortedLis. Find the previous index if date does not exist in teh SortedLIst</summary>
        /// <param name="date">date to be looked up</param>
        /// <returns>ix of the date. Return the previous index if the date does not exists</returns>
        private int getPositionIxOfDate(DateTime date)
        {
            int posIx = this.positionsM.IndexOfKey(date);
            if (posIx == -1)
                posIx = DateTimeUtil.ixOfLessOrEqual(this.positionsM.Keys, date);
            return posIx;
        }

        /// <summary>Find date's index in price SortedList. Find the previous index if date does not exist in the SortedLIst</summary>
        /// <param name="date">date to be looked up</param>
        /// <returns>ix of the date in pricesM. Return the previous index if the date does not exists</returns>
        private int getPriceIxOfDate(DateTime date)
        {
            int posIx = this.pricesM.IndexOfKey(date);
            if (posIx == -1)
                posIx = DateTimeUtil.ixOfLessOrEqual(this.pricesM.Keys, date);
            return posIx;
        }
    }
}
